// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: xguo

#pragma once

#include <algorithm>
#include <cstdio>
#include <limits>
#include <memory>
#include <string>

#include "coin/proto/coin_order_enums.pb.h"
#include "coin2/base/config/app_config.h"
#include "coin2/base/log.h"
#include "coin2/base/numberic_util.h"
#include "coin2/exchange/base/symbology/currency.h"
#include "coin2/exchange/base/symbology/symbol.h"
#include "coin2/exchange/base/symbology/util.h"

namespace coin2::exchange::base::symbology {

class TickQty {
 public:
  virtual ~TickQty() = default;
  virtual int max_precision() const = 0;
  virtual double submittable_min_qty() const = 0;
  virtual double submittable_min_qty(double price) const = 0;
  virtual double Ceil(double qty) const = 0;
  virtual double Floor(double qty) const = 0;
  virtual double Round(double qty) const = 0;
  virtual double RoundAggressively(double qty, coin::proto::OrderDirection dir) const = 0;
  virtual double RoundPassively(double qty, coin::proto::OrderDirection dir) const = 0;
  virtual std::string Format(double qty) const = 0;
  virtual auto TickSize() const -> double = 0;

  double RoundWithPolicy(
      double raw_qty,
      coin::proto::OrderDirection dir,
      coin2::strategy::TickRounding policy) const {
    using coin2::strategy::TickRounding;
    switch (policy) {
      case TickRounding::TICK_ROUND_UNINIT:
        THROW();
        return 0.;
      case TickRounding::TICK_ROUND_OUTWARD:
        return RoundAggressively(raw_qty, dir);
      case TickRounding::TICK_ROUND_INWARD:
        return RoundPassively(raw_qty, dir);
      case TickRounding::TICK_ROUND_NEAREST:
        return Round(raw_qty);

      case TickRounding::TICK_ROUND_FLOOR:
        return Floor(raw_qty);
      case TickRounding::TICK_ROUND_CEIL:
        return Ceil(raw_qty);
      default:
        THROW();
    }
    return 0;
  }
};

class FixedTickQty : public TickQty {
 public:
  FixedTickQty(double tick_size, double min_qty, double max_qty)
      : tick_size_(tick_size),
        min_qty_(min_qty),
        max_qty_(max_qty),
        min_amount_(0.0),
        format_str_(fmt::format("{{:.{}f}}", impl::CalculatePrecision(tick_size))),
        precision_(impl::CalculatePrecision(tick_size)) {
    CHECK_GE(tick_size_, 0) << tick_size_;
    CHECK_GE(min_qty_, 0) << min_qty_;
    CHECK_GE(max_qty_, min_qty_) << min_qty_ << "," << max_qty_;
  }

  explicit FixedTickQty(double tick_size)
      : tick_size_(tick_size),
        min_qty_(0.0),
        max_qty_(std::numeric_limits<double>::max()),
        min_amount_(0.0),
        format_str_(fmt::format("{{:.{}f}}", impl::CalculatePrecision(tick_size))),
        precision_(impl::CalculatePrecision(tick_size)) {
    CHECK_GE(tick_size_, 0) << tick_size_;
    CHECK_GE(min_qty_, 0) << min_qty_;
    CHECK_GE(max_qty_, min_qty_) << min_qty_ << "," << max_qty_;
  }

  explicit FixedTickQty(double tick_size, double min_qty, double max_qty, double min_amount)
      : tick_size_(tick_size),
        min_qty_(min_qty),
        max_qty_(max_qty),
        min_amount_(min_amount),
        format_str_(fmt::format("{{:.{}f}}", impl::CalculatePrecision(tick_size))),
        precision_(impl::CalculatePrecision(tick_size)) {
    CHECK_GE(tick_size_, 0) << tick_size_;
    CHECK_GE(min_qty_, 0) << min_qty_;
    CHECK_GE(max_qty_, min_qty_) << min_qty_ << "," << max_qty_;
  }
  double Ceil(double qty) const override {
    constexpr double buffer = 1.e-10;
    return Constrain(std::ceil(qty / tick_size_ - buffer) * tick_size_);
  }

  double Floor(double qty) const override {
    constexpr double buffer = 1.e-10;
    return Constrain(std::floor(qty / tick_size_ + buffer) * tick_size_);
  }

  double Round(double qty) const override {
    return Constrain(std::round(qty / tick_size_) * tick_size_);
  }

  double RoundAggressively(double qty, coin::proto::OrderDirection dir) const override {
    using coin::proto::OrderDirection;
    if (dir == OrderDirection::DIRECTION_BUY) {
      return std::ceil(qty / tick_size_) * tick_size_;
    }
    if (dir == OrderDirection::DIRECTION_SELL) {
      return std::floor(qty / tick_size_) * tick_size_;
    }
    THROW() << "Unimplemented for OrderDir " << dir;
    return 0;
  }
  double RoundPassively(double qty, coin::proto::OrderDirection dir) const override {
    using coin::proto::OrderDirection;
    if (dir == OrderDirection::DIRECTION_BUY) {
      return std::floor(qty / tick_size_) * tick_size_;
    }
    if (dir == OrderDirection::DIRECTION_SELL) {
      return std::ceil(qty / tick_size_) * tick_size_;
    }
    THROW() << "Unimplemented for OrderDir " << dir;
    return 0;
  }

  std::string Format(double qty) const override { return fmt::format(format_str_, qty); }

  int max_precision() const override { return precision_; }

  // if min qty is not available, the submittable min qty should be at least 1 tick
  double submittable_min_qty() const override { return std::max(min_qty_, tick_size_); }

  double submittable_min_qty(double price) const override {
    return Ceil(std::max({min_qty_, min_amount_ / price, tick_size_}));
  }

  // double RemoveEpsilion(double qty) const override  {  // TODO
  //   return 0;
  // }

  auto TickSize() const -> double override { return tick_size_; }

 private:
  double Constrain(double qty) const {
    CHECK_GE(qty, 0) << "Invalid negative quantity.";
    if (min_qty_ <= qty && qty <= max_qty_) {
      return qty;
    } else if (qty > max_qty_) {
      return max_qty_;
    } else if (qty < min_qty_) {
      return min_qty_;
    }
    NOTREACHED() << "min_qty: " << min_qty_ << " "
                 << "max_qty: " << max_qty_;
    return 0;
  }

 private:
  const double tick_size_;
  const double min_qty_;
  const double max_qty_;
  const double min_amount_;
  const std::string format_str_;
  const int precision_;
};

}  // namespace coin2::exchange::base::symbology
